Skip to content

fix(secrets,worktrees): fix secret redaction leakage and prune stale worktrees - #855

Open
euxaristia wants to merge 33 commits into
Gitlawb:mainfrom
euxaristia:fix/secrets-redaction-and-worktrees
Open

fix(secrets,worktrees): fix secret redaction leakage and prune stale worktrees#855
euxaristia wants to merge 33 commits into
Gitlawb:mainfrom
euxaristia:fix/secrets-redaction-and-worktrees

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Closes a tail-leak edge case in secret redaction and adds an anthropic_key pattern; extra secret values are sorted by length descending so a shorter match can't shadow a longer one, and worktree paths are canonicalized before comparison
  • Adds recoverable PID-based leases for worktrees: reclaims released or crashed worktrees, proves Prepare ownership with a git-admin marker, and scopes unlock/release/clean to locks actually owned by the caller
  • Fixes real data-loss risks: preserves orphaned commits instead of discarding them, verifies release ownership before touching a worktree, checks removal exit codes and fails closed on inspection errors, and handles os.Chtimes errors and legacy worktree cleanup on reuse
  • Adds -C to worktrees release, completes worktrees in shell completions, and adds regression tests for the aggregation, lease-detection, and nested-activity paths this touches

Test plan

  • go test ./internal/worktrees/... ./internal/secrets/... ./internal/cli/...

Summary by CodeRabbit

  • New Features

    • Added worktree release to manually unlock worktrees, with path handling, recovery guidance, and clear confirmation messages.
    • Worktree preparation now tracks ownership, prevents conflicting access, and automatically releases locks acquired during a run.
    • Added safer cleanup of stale worktrees while preserving relevant repository history.
  • Bug Fixes

    • Improved detection and redaction of API keys, credentials, JWTs, and other sensitive tokens across command output and error messages.
    • Enhanced cross-platform handling of expired worktree leases.

euxaristia and others added 26 commits July 22, 2026 12:50
…worktrees

1. Prevent trailing redaction leaks in github_token, aws_access_key_id, and google_api_key by adding trailing word boundaries and allowing variable lengths. Refine the openai_key pattern to cleanly distinguish legacy keys and modern prefixed keys (sk-proj-, sk-svcacct-) from ordinary kebab-case phrases.
2. Implement auto-pruning of zero-owned git worktrees older than 24 hours at the start of worktrees.Prepare to prevent indefinite disk space leaks.
…oss risk

Drop the trailing \b anchor on the four secret patterns whose body
class allows "-" (slack_token, google_api_key, the modern openai_key
branch, jwt). \b requires a word/non-word transition, so a secret
ending in "-" right before a delimiter has none, and the engine
backtracked the greedy quantifier to drop that last character instead
of failing the match, leaking it. The body character class already
provides the real stopping boundary, so the anchor was unnecessary.

Fix two issues in worktree Clean flagged in review:

- Staleness was decided by the worktree directory's own mtime, which
  only changes when an entry is added/removed/renamed directly inside
  it, not when a long-running task edits existing files deeper in the
  tree. Clean now walks the tree and treats any recently modified entry
  as live, and also skips any worktree a caller has explicitly locked
  via git worktree lock.
- baseDir ownership used a raw strings.HasPrefix, so a sibling like
  "<baseDir>-other" would false-match. Replaced with a filepath.Rel
  path-boundary check.
…n errors

defaultRunGit deliberately returns a nil error alongside a nonzero
CommandResult.ExitCode for a failed git invocation, so the worktree
remove call must check ExitCode itself instead of trusting a nil error
to mean success. Route it through gitOutput, which already does that.

worktreeIsStale treated an inspection failure (an unreadable file, a
WalkDir error) the same as "keep walking," which can let an
incompletely-inspected worktree be judged stale. Any inspection error
now makes it ineligible for removal instead.
…to owned worktrees

The scanner's trailing \b anchors made a credential vanish entirely when
followed by a word character outside its body class (an appended suffix
like AKIA...EXTRA, or ghp_..._suffix): the fixed or unbounded-greedy
quantifier had no valid word boundary to land on and the whole match
failed, so the real secret reached the redaction output unredacted.
Dropping the trailing anchors lets the body class itself stop the match,
so the credential prefix still gets redacted even when noise follows it.
Also recognize sk-admin- alongside sk-proj-/sk-svcacct- so OpenAI admin
keys aren't skipped by the narrowed modern-key branch.

Clean pruned any worktree under the caller-supplied BaseDir, but Prepare
only ever creates worktrees under a per-repository
zero-worktree-<repoKey> subtree of it. Scope pruning to that subtree so a
worktree a user manages by hand elsewhere under a shared BaseDir is never
force-removed. Also refuse to force-remove a worktree whose mtime looks
stale but that still has uncommitted or untracked changes: a task can
hold live work while waiting on a model, network, or user for longer
than the staleness window without writing to the tree again.
…laims to

activePath/internal was created by the same MkdirAll as the nested pkg
dir but never backdated, so it kept a fresh mtime and worktreeIsStale's
walk reported "not stale" as soon as it hit that directory, before ever
reaching the freshly-written file two levels deeper. The test passed
without actually exercising recursion past the first directory.
…s dirty

Prepare never called git worktree lock, so the entry.locked skip in Clean
only ever protected worktrees a human locked by hand, never zero's own; a
worktree that finished committing and sat idle (e.g. waiting on a slow
model or network retry) for more than 24h looked clean-and-stale and got
force-removed by the mtime+dirty heuristic alone. Lock every worktree
Prepare creates so it gets the same protection.

worktreeIsDirty also used git status --porcelain with no --ignored, so a
worktree holding only .gitignore-matched task data (credentials, generated
drafts, artifacts) reported as clean and got force-removed with --force,
silently discarding it. Add --ignored so those files count as dirty too.
…shed worktrees

Prepare locks every worktree it creates so Clean's mtime+dirty staleness
heuristic never force-removes one Zero is still using, but nothing ever
unlocked it, making the automatic disk-space cleanup permanently inert.

Add Release (git worktree unlock) and wire it in two ways: zero exec
--worktree defers a release once its own run finishes, since that flow's
use of the worktree is bound to its own process. zero worktrees prepare
hands the path to a longer-lived external caller with no defined
end-of-life, so a new zero worktrees release <path> subcommand lets that
caller release it explicitly when done.
…k deleted worktrees

Address CodeRabbit's review on the lock-release fix:

- zero worktrees release now resolves its path argument to absolute
  before calling Release, since git worktree unlock matches against
  the path git recorded at creation, not whatever directory the
  caller happens to be running from.
- Clean now aggregates removal failures with errors.Join instead of
  overwriting lastErr, so multiple stale worktrees failing removal in
  the same pass are all reported, not just the last one.
- Release falls back to options.Cwd as the git working directory when
  the worktree path itself no longer exists (e.g. a caller deleted a
  locked worktree by hand instead of releasing it first), so the
  orphaned lock can still be cleared.

Added regression coverage for all three.
- Prepare re-locks a reused worktree so Clean's staleness heuristic cannot
  force-remove it while the new caller is still using it; a lock already
  held by a live external caller is kept in place and reported through the
  new Result.LockAcquired field.
- exec --worktree only releases the lock its own Prepare call acquired and
  surfaces a failed release on stderr with the affected path instead of
  discarding the error.
- worktrees release wires the resolved workspace root into Options.Cwd so
  the deleted-path recovery works outside the worktree directory.
- The relative-path release test derives its expected value via
  filepath.Abs, matching the resolution the CLI uses, so macOS /var vs
  /private/var spellings no longer break it.
…d release -C

- Prepare rejects a worktree whose lock another run still holds, on both
  the reuse and the create-race paths, instead of handing a second live
  caller an unprotected shared checkout whose sole Git lock the first
  caller's exit would release.
- The automatic stale-worktree pruning runs only after the request itself
  validates, so a rejected command (an invalid --name) has no destructive
  cleanup side effect; covered end to end with real git in both directions.
- worktrees release accepts -C/--cwd naming the source repository, which
  the deleted-path recovery needs when launched outside the repo (the
  deleted worktree path is a one-way hash with no way back to its source).
git records worktree paths in physical form, so the CI runners' symlinked
(/var -> /private/var) and 8.3-short (RUNNER~1) temp spellings made Clean's
containment check skip the test's stale entry and the pruning assertion
fail on macOS and Windows.
…canonicalize base dir

Four fixes from the latest review round:

- Clean now creates a durable ref (refs/zero/orphaned-worktree/<sha>)
  for a detached worktree's HEAD before force-removing it, when that
  commit isn't already reachable from any other ref. Prepare always
  creates worktrees with `worktree add --detach`, so a commit made
  there had no ref pointing at it once the worktree was deleted,
  making it immediately eligible for git gc despite never having been
  merged/pushed elsewhere.

- Clean resolves its configured base directory through EvalSymlinks
  before comparing it against git's reported worktree paths: `git
  worktree list --porcelain` reports each worktree's PHYSICAL location
  (resolving symlink components), so a symlinked --worktree-dir made
  every worktree created under it permanently unprunable.

- Release now verifies path has a zero-worktree-<repoKey> ancestor
  directory component before running `git worktree unlock`, so it
  can't be used to clear the lock on a worktree a user or another tool
  manages by hand. The check doesn't need to know which --dir a given
  Prepare call used (nothing records that against a specific worktree,
  and the CLI never threads BaseDir through to Release) — the repoKey
  component is Prepare's actual ownership signature regardless of
  which directory it was created under.

- Route the release command's printed path and exec's release-failure
  diagnostic through the existing CLI redaction helper, and split the
  worktrees help text into prepare-specific and release-specific flag
  sections (release only ever supported -C/--cwd, not the --name/--dir/
  --json the shared block advertised).

All four have regression tests confirmed to fail without their fix
(two using real git worktrees, not just fakeRunner sequences). Build,
vet, and gofmt clean on linux/windows/darwin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rees

Two review findings on the cleanup lifecycle:

- A lock left by an abnormal exit (SIGKILL, crash, power loss) was
  skipped by Clean forever, recreating the permanent disk leak this PR
  set out to fix. exec --worktree now records its PID in the lease
  reason; Clean expires a lease whose recorded owner is provably dead,
  unlocking only after the staleness, dirty, and HEAD-preservation
  guards all pass. Human locks and PID-less leases (external
  `worktrees prepare` owners) remain permanent until explicit release,
  and any ambiguity in the liveness probe counts as alive.

- An explicitly released worktree holding only gitignored residue
  (node_modules, build output) was skipped at every age. Release is the
  owner's completion signal, so unlocked entries now block removal only
  on tracked/untracked changes; expired crashed leases keep the
  conservative --ignored probe since they never signaled completion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e safety

Split dead-lease PID checking into posix/windows implementations so
Windows can reliably tell a dead process from a live one. Fix a path
canonicalization mismatch in two release tests. Derive release
ownership from git worktree list instead of the git-dir parent, which
was wrong for repos with a separate git-dir. Refuse to clear a lock
that was not taken by Zero in the first place.
Compare Clean containment and Release ownership against physical path
spellings so macOS /var vs /private/var and symlink TMPDIR layouts match
git worktree list. Require a registered porcelain entry and a Zero lease
reason before unlock; treat an already-unlocked Zero worktree as a no-op.
- Prepare and Release now agree on repoKey regardless of which worktree
  (main or linked) Prepare runs from, by keying off git worktree list's
  first entry (always the main worktree) instead of --show-toplevel.
  A worktree prepared from a linked checkout previously failed its own
  ownership check on release and its lease could never be cleared.
- osProcessAlive on Windows no longer treats every OpenProcess failure
  as "process is dead": only ERROR_ACCESS_DENIED (a live process this
  caller lacks rights to query) is now distinguished from a genuinely
  missing PID, so Clean can no longer force-remove an active worktree
  whose owning process it simply couldn't query.
- The openai_key redaction pattern now recognizes sk-or-v1- (OpenRouter)
  alongside the existing sk-proj-/sk-svcacct-/sk-admin- prefixes, so
  hyphenated OpenAI-compatible provider keys are redacted again without
  reopening the sk-<kebab-case-phrase> false-positive this pattern was
  narrowed to avoid.
- Release/exec --worktree error text is redacted before reaching
  stderr, matching the already-redacted success path; ownership errors
  interpolate the caller-supplied path, which could carry a key-shaped
  segment.
- canonicalizePath resolves symlinks through the nearest existing
  ancestor when the target itself no longer exists, so the documented
  `release -C` recovery path works again for a worktree deleted by hand
  under a symlinked --worktree-dir.
- Prepare rolls back the worktree `git worktree add` just created if the
  subsequent lock call fails for a reason other than a concurrent
  racer, instead of leaking an unleased checkout until Clean's 24h
  staleness window reclaims it.

Not addressed here: the P2 finding that worktree ownership is provable
only by a directory-name convention plus a lock-reason prefix, both of
which a user can reproduce by hand. A durable per-worktree ownership
marker would close that gap, but internal/worktrees has been on main
since Gitlawb#70, so a marker requirement could reject worktrees an
already-installed zero created before this change existed. Needs a
decision on migration before implementing.
Address review findings that path convention plus lease-reason prefix are
forgeable by hand. Prepare now writes a zero-owner marker into the worktree
admin dir; Release and Clean require it before force-touching a path. Clean
also keys its owned subtree off the main worktree root so linked-checkout
calls still prune the same bucket Prepare uses. Tests plant the marker and
list the main worktree first so fixtures match production.
…ease rejection, and completions

- TestCleanFromLinkedWorktreePrunesStaleWorktree: pins Clean deriving its
  owned-subtree key from the main worktree root (not the invoking linked
  checkout's --show-toplevel) so Prepare/Clean run from a linked worktree
  actually reclaim the worktrees Prepare created there.
- TestReleaseRejectsForgedZeroLeaseWithoutOwnershipMarker: pins the
  ownership-marker requirement against the exact forgery jatmn described - a
  worktree under the predictable zero-worktree-<repoKey> path, manually
  locked with a reason that merely starts with the zero lease prefix.
- Fix TestPrepareCreatesDetachedGitWorktree: its fake git runner ran out of
  canned results at Prepare's post-lock ownership-marker write, so
  writeOwnershipMarker resolved gitDir to "" and os.WriteFile wrote
  "zero-owner" as a relative path into the test process's real working
  directory instead of failing loudly. Give it autoAbsoluteGitDir like the
  other Prepare-exercising tests use and assert on the marker-write call.
- completions_test.go: assert `worktrees`/`worktree` completions include
  `release` alongside `prepare`.
When Prepare reuses an existing worktree, it now touches the directory
to refresh its mtime. This prevents Clean from force-removing a
long-running but idle worktree (e.g. waiting on a model) that has no
recent file changes but is still actively in use.

Addresses the data-loss risk flagged in the PR review where mtime-only
staleness + --force removal could discard live worktrees.

Co-authored-by: cairn-code
Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
…ot to primaryRoot, and add anthropic_key pattern

Unlock using matched porcelain entry path, set Result.RepoRoot to primaryRoot in Prepare, add dedicated anthropic_key secret pattern, and canonicalize fake-runner test paths.

Refs Gitlawb#632
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c2071c95-d08f-41e6-8af5-1729a2e909b8

📥 Commits

Reviewing files that changed from the base of the PR and between d2030d6 and a29900f.

📒 Files selected for processing (6)
  • internal/cli/workflows.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_test.go
  • internal/zerocommands/backend_snapshots_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/secrets/scanner.go
  • internal/cli/workflows.go
  • internal/worktrees/worktrees_test.go
  • internal/secrets/scanner_test.go
  • internal/worktrees/worktrees.go

Walkthrough

The change adds worktree locking, release, lease recovery, cleanup, and CLI integration. It also expands secret detection and redaction for additional credential formats, boundary cases, overlapping secrets, and false-positive handling.

Changes

Worktree lifecycle

Layer / File(s) Summary
Worktree preparation and ownership
internal/worktrees/worktrees.go, internal/worktrees/worktrees_test.go
Preparation resolves the primary worktree, acquires locks, records PID leases, writes ownership markers, and reports lock ownership.
Release and stale cleanup
internal/worktrees/*, internal/worktrees/worktrees_test.go
Release validates ownership before unlocking. Cleanup reclaims eligible stale worktrees, preserves unreachable commits, and uses platform-specific process checks.
Worktree release CLI
internal/cli/app.go, internal/cli/workflows.go, internal/cli/completions.go, internal/cli/workflow_test.go
The CLI adds release dispatch, path and cwd handling, dependency wiring, completion entries, help text, redacted errors, and confirmation output.
Exec lock ownership cleanup
internal/cli/exec.go, internal/cli/workflow_test.go
Exec records the process PID and releases only locks acquired by the current run. Release failures are reported with redaction.

Secret detection and redaction

Layer / File(s) Summary
Credential pattern coverage
internal/secrets/scanner.go, internal/secrets/scanner_test.go, internal/tools/bash_secrets_test.go
Scanning adds Anthropic keys, broader OpenAI formats, longer GitHub and Google tokens, loose JWTs, and boundary and false-positive handling.
Overlapping secret replacement
internal/redaction/redaction.go, internal/redaction/audit_fixes_test.go
Redaction sorts extra secrets by length and applies OpenAI-specific matching before general patterns. Tests cover overlaps, suffixes, trailing hyphens, and false positives.
Redaction regression coverage
internal/cli/*_test.go, internal/tui/*_test.go, internal/zerogit/*_test.go, internal/selfverify/*_test.go, internal/sessions/*_test.go, internal/zerocommands/*_test.go
Fixtures and assertions verify complete removal of longer and newly recognized credentials across CLI, TUI, verification, Git, and command-output paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Worktrees
  participant Git
  CLI->>Worktrees: prepare worktree with LeasePID
  Worktrees->>Git: acquire worktree lock
  Git-->>Worktrees: return lock status
  Worktrees-->>CLI: return LockAcquired
  CLI->>Worktrees: release acquired lock
  Worktrees->>Git: unlock worktree
Loading

Possibly related PRs

  • Gitlawb/zero#632: Modifies the same worktree locking, release, and secret-redaction implementations.
  • Gitlawb/zero#829: Modifies internal/worktrees/worktrees.go and related worktree lifecycle behavior.

Suggested reviewers: gnanam1990, vasanthdev2004, jatmn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: secret redaction fixes and stale worktree pruning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…test

TestFormatBashOutputRedactsAnthropicKey checked for the openai_key
placeholder instead of anthropic_key, a copy-paste leftover. The
redaction itself was already correct; only the assertion was wrong.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
internal/worktrees/worktrees_posix.go (1)

20-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Default unknown signal errors to alive, so the fail-closed contract holds.

processAlive documents that any ambiguity counts as alive, so an uncertain answer can never expire a lease. This implementation returns alive only for nil and EPERM. Every other error, including an unexpected one, returns false and marks the lease expired. That path lets Clean unlock and force-remove a worktree based on an inconclusive probe.

Treat only the definite "no such process" answers as dead.

🛡️ Proposed fix
 	err = proc.Signal(syscall.Signal(0))
 	if err == nil {
 		return true
 	}
-	if errors.Is(err, os.ErrProcessDone) {
+	// Only ESRCH / ErrProcessDone prove the PID names no live process. Any
+	// other error is inconclusive, so report alive and keep the lease.
+	if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
 		return false
 	}
-	return errors.Is(err, syscall.EPERM)
+	return true
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_posix.go` around lines 20 - 27, Update the error
handling in processAlive so only the definite no-process result
(os.ErrProcessDone) returns false; retain true for successful probes and
permission errors, and default all other unexpected signal errors to true to
preserve the fail-closed lease behavior.
internal/cli/workflow_test.go (2)

325-325: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two secret scanners flag these synthetic key literals.

The literals are clearly fake fixtures for the redaction assertions, and the tests are valuable. Both Betterleaks and ast-grep report them as hard-coded credentials, which adds recurring noise to every scan of this file. Assemble the fixture at runtime from fragments, or add the scanners' inline allow annotation, so the finding does not have to be triaged again.

♻️ Proposed fix
-	secret := "sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu"
+	// Assembled at runtime so secret scanners do not report this synthetic
+	// redaction fixture as a hard-coded credential.
+	secret := "sk-" + "proj-abcDEF123_ghiJKL456-mnoPQR789stu"

Also applies to: 933-933

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` at line 325, Update the synthetic secret
fixtures in the workflow tests, including both occurrences of the key literal,
so scanners no longer classify them as hard-coded credentials. Assemble each
fixture at runtime from non-secret fragments, or apply the repository-supported
inline allow annotation while preserving the existing redaction assertions.

Source: Linters/SAST tools


187-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use t.Chdir for the workflow test temporary directory.

go.mod declares Go 1.26.5, so testing.T.Chdir is available. Use it instead of manual os.Chdir plus deferred restore; it restores the working directory automatically during cleanup.

♻️ Proposed fix
-	origWd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
 	parent := t.TempDir()
 	worktreeDir := filepath.Join(parent, "task-a")
 	if err := os.Mkdir(worktreeDir, 0o700); err != nil {
 		t.Fatal(err)
 	}
-	if err := os.Chdir(parent); err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		if err := os.Chdir(origWd); err != nil {
-			t.Fatal(err)
-		}
-	}()
+	t.Chdir(parent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 187 - 203, Update the workflow
test’s working-directory setup to use testing.T.Chdir with the temporary parent
directory, and remove the manual os.Getwd, os.Chdir, and deferred restoration
logic. Preserve the existing worktreeDir creation and test behavior.
internal/cli/workflows.go (1)

133-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three small CLI UX inconsistencies in the release path.

  1. Line 180 prints the raw path argument, but line 177 releases absPath. A user who runs zero worktrees release task-a sees released task-a while git unlocked an absolute path. Report the path that was acted on.
  2. Line 134 does not trim the --cwd= value, while parseWorktreeCommandArgs trims the same flag at line 405. --cwd=" " then reaches resolveWorkspaceRoot as whitespace.
  3. In the help text, -h, --help is now the last entry of the release flags: block, so it reads as release-only. It applies to both subcommands.
♻️ Proposed fixes
 		case strings.HasPrefix(arg, "--cwd="):
-			cwdFlag = strings.TrimPrefix(arg, "--cwd=")
+			cwdFlag = strings.TrimSpace(strings.TrimPrefix(arg, "--cwd="))
-	if _, err := fmt.Fprintf(stdout, "released %s\n", redactCLIString(path)); err != nil {
+	if _, err := fmt.Fprintf(stdout, "released %s\n", redactCLIString(absPath)); err != nil {
 		return exitCrash
 	}
 release flags:
   -C, --cwd <path>        Source repository directory (required if the
                            worktree directory was already deleted)
+
+common flags:
   -h, --help              Show this help

Also applies to: 180-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflows.go` around lines 133 - 134, Update the release
workflow to report the acted-on absolute path variable (absPath) instead of the
raw path argument, trim whitespace from the --cwd= value when assigning cwdFlag
in argument parsing, and move the -h, --help entry in the release flags help
text so it clearly applies to both release subcommands.
internal/worktrees/worktrees_test.go (1)

1268-1272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two doc comments describe tests that are not below them.

At lines 1268-1272 a comment about --ignored blocking force-removal sits directly above TestCleanPreservesUnreachableCommitBeforeRemoval, which tests commit preservation. At lines 1547-1553 a comment about worktreeIsDirty and --ignored sits above TestCleanHonorsTouchLiveness. It looks like the tests those paragraphs belonged to moved. Move each paragraph onto its own test or delete it, so the next reader does not trust the wrong description.

Also applies to: 1547-1553

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_test.go` around lines 1268 - 1272, Correct the
misplaced doc comments in internal/worktrees/worktrees_test.go: move the
--ignored/worktreeIsDirty force-removal explanation to the test that verifies
ignored-only content is treated as dirty, and move the commit-preservation
explanation to TestCleanPreservesUnreachableCommitBeforeRemoval. Ensure each
paragraph directly precedes the test it describes, or remove it if no matching
test exists.
internal/worktrees/worktrees.go (1)

284-294: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a locale-stable signal for git worktree lock collisions.

lockWorktree treats already locked as a non-lock failure, but that message is translated under LANG/LC_MESSAGES. A localized message can cause Prepare to classify a live worktree collision as a real failure and roll back/remove the newly created worktree. Run the lock through defaultRunGit with LC_ALL=C, or use a locale-independent signal such as the lock exit code plus another stable Git state (for example, querying the worktree list before running add).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees.go` around lines 284 - 294, Update lockWorktree
to classify existing-lock collisions using a locale-stable signal instead of
matching the localized “already locked” text. Run git worktree lock through
defaultRunGit with LC_ALL=C, or use the lock exit code together with another
stable Git-state check, while preserving successful locks and genuine error
propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/redaction/redaction.go`:
- Around line 160-168: Update the textSecretPatterns used by RedactString to
match secrets.Scan’s end-boundary behavior and supported secret formats,
removing trailing \b behavior that misses appended suffixes or leaves terminal
hyphens after backtracking. Add parity tests exercising RedactString with
appended suffixes and secrets ending in hyphens.

In `@internal/tools/bash_secrets_test.go`:
- Around line 28-36: Update TestFormatBashOutputRedactsAnthropicKey to store the
complete Anthropic key fixture in a key variable, pass it into formatBashOutput,
and assert that the resulting output does not contain the full key while
retaining the typed redaction-placeholder assertion.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1474-1481: Update the Clean-related fixtures, including the test
covering ignored data and TestCleanSkipsWorktreeWithRecentNestedActivity,
TestCleanHonorsLiveLease, TestCleanHonorsTouchLiveness, and
TestCleanSkipsDirtyStaleWorktree, to list the main worktree rooted at repoRoot
before the target entry, matching TestCleanRecoversExpiredLease. Pass tempDir
through physicalTestPath where required, and add positive assertions for the
expected git call sequence so the tests genuinely reach the lease, status, and
removal logic.

---

Nitpick comments:
In `@internal/cli/workflow_test.go`:
- Line 325: Update the synthetic secret fixtures in the workflow tests,
including both occurrences of the key literal, so scanners no longer classify
them as hard-coded credentials. Assemble each fixture at runtime from non-secret
fragments, or apply the repository-supported inline allow annotation while
preserving the existing redaction assertions.
- Around line 187-203: Update the workflow test’s working-directory setup to use
testing.T.Chdir with the temporary parent directory, and remove the manual
os.Getwd, os.Chdir, and deferred restoration logic. Preserve the existing
worktreeDir creation and test behavior.

In `@internal/cli/workflows.go`:
- Around line 133-134: Update the release workflow to report the acted-on
absolute path variable (absPath) instead of the raw path argument, trim
whitespace from the --cwd= value when assigning cwdFlag in argument parsing, and
move the -h, --help entry in the release flags help text so it clearly applies
to both release subcommands.

In `@internal/worktrees/worktrees_posix.go`:
- Around line 20-27: Update the error handling in processAlive so only the
definite no-process result (os.ErrProcessDone) returns false; retain true for
successful probes and permission errors, and default all other unexpected signal
errors to true to preserve the fail-closed lease behavior.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1268-1272: Correct the misplaced doc comments in
internal/worktrees/worktrees_test.go: move the --ignored/worktreeIsDirty
force-removal explanation to the test that verifies ignored-only content is
treated as dirty, and move the commit-preservation explanation to
TestCleanPreservesUnreachableCommitBeforeRemoval. Ensure each paragraph directly
precedes the test it describes, or remove it if no matching test exists.

In `@internal/worktrees/worktrees.go`:
- Around line 284-294: Update lockWorktree to classify existing-lock collisions
using a locale-stable signal instead of matching the localized “already locked”
text. Run git worktree lock through defaultRunGit with LC_ALL=C, or use the lock
exit code together with another stable Git-state check, while preserving
successful locks and genuine error propagation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06ea9d09-39af-4492-995c-e3afd7f986f2

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and 29f1d87.

📒 Files selected for processing (16)
  • internal/cli/app.go
  • internal/cli/completions.go
  • internal/cli/completions_test.go
  • internal/cli/exec.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/tools/bash_secrets_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_posix.go
  • internal/worktrees/worktrees_test.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees_windows_test.go

Comment thread internal/redaction/redaction.go
Comment thread internal/tools/bash_secrets_test.go
Comment thread internal/worktrees/worktrees_test.go
Align RedactString token boundaries with secrets.Scan, strengthen the
Anthropic bash redaction assertion, fix Clean fixtures so they reach
the guards under test, and fail closed on ambiguous processAlive probes.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit findings in c9a347a:

  1. RedactString parity with secrets.Scan — removed trailing \b anchors; aligned formats/boundaries; added suffix and trailing-hyphen parity tests.
  2. Anthropic bash test — asserts the full key is absent, not only the prefix.
  3. Clean fixtures — list main worktree first, use physicalTestPath, and positively assert the git call sequence reaches the guard under test.
  4. processAlive — only ESRCH/ErrProcessDone count as dead; ambiguous errors keep the lease.
  5. CLI release path: print absPath, trim --cwd=, help text for -h.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

@euxaristia I will review the complete PR, including the changes in c9a347a.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The worktree lease/ownership work is genuinely good — the marker-in-the-git-admin-dir trick is the right ownership proof, the fail-closed staleness and dirty probes are careful, and I mutation-tested a couple of the guards (deleting the ownership-marker rejection and forcing includeIgnored=false both turn the right tests red, so they're real). But two things need fixing before this can land: the redaction change is a net loss of coverage relative to main, and the new auto-Clean destroys data on upgrade.

The openai_key pattern got narrower, not wider

internal/secrets/scanner.go:49 (and its mirror at internal/redaction/redaction.go:79) went from

\bsk-[A-Za-z0-9_-]{20,}

to

\bsk-(?:proj-|svcacct-|admin-|or-v1-)[A-Za-z0-9_-]{20,}|\bsk-[A-Za-z0-9]{20,}

The fallback branch no longer allows - or _, so any sk-<vendor>-<body> key whose vendor segment isn't one of the four you enumerated matches nothing at all. I drove this through the real entry point (formatBashOutputsecrets.Redact) with sentinel tokens:

sk-fw-SENTINEL_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa        -> leaked verbatim
sk-live-SENTINELaaaaaaaaaa_bbbbbbbbbb-cccc           -> leaked verbatim

Both are matched by main's regex. Same result through redaction.RedactString; I compiled origin/main:internal/redaction/redaction.go standalone and ran the identical inputs — main returns value: [REDACTED] for both.

What makes this blocking rather than a judgement call is internal/cli/sandbox_check_test.go:155. TestRunSandboxCheckMatchedGrantRedactsReason already existed on main and pinned that a grant reason containing sk-test-secret1234567890 never reaches zero sandbox check --json. This PR rewrites that fixture to sk-proj-testsecret1234567890ab. On this branch:

RedactString("approved with sk-test-secret1234567890", Options{})
  == "approved with sk-test-secret1234567890"

So the fixture was changed because the behaviour regressed, and the test that would have caught it can't anymore. Please restore the original fixture.

I understand the motivation — TestScanIgnoresKebabCaseStartingWithSk shows sk-learn-machine-learning-model was a false positive on main. But you can kill that without giving up the general branch. RE2 has no lookahead, so do it in Go: keep \bsk-[A-Za-z0-9_-]{20,} and drop an openai_key match whose body contains no digit. sk-learn-machine-learning-model has none; every real key does. Tighten further if you want, but the enumerated-prefix approach means every new vendor prefix is a silent miss until someone notices.

Same file, internal/redaction/redaction.go:87: the JWT pattern now requires the second segment to start with eyJ. That's fine for a standard JWS but not for a compact JWS with a non-JSON payload or for a JWE, where segment two is an encrypted key. Probed:

eyJhbGciOiJIUzI1NiJ9.U0VOVElORUxwYXlsb2Fk.SENTINELsignature123
  main: [REDACTED]      this branch: leaked in full
5-part JWE
  main: [REDACTED].SENTINELciphertext.SENTINELtag12345    this branch: leaked in full

Please keep the looser alternative alongside the strict one.

Clean destroys pre-upgrade worktrees' gitignored contents

internal/worktrees/worktrees.go:858 runs worktreeIsDirty(..., expiredLease) before the ownership/legacy determination at :871-880. For a legacy entry — unlocked, no marker, created by the version of Prepare that's on main today — expiredLease is false, so the probe omits --ignored and the worktree is treated as explicitly released. But release didn't exist in the version that created it; unlocked was the only state there ever was.

Reproduced end to end with real git: created a worktree exactly the way main's Prepare does (git worktree add --detach under <base>/zero-worktree-<key>/legacy-task, no lock, no marker), dropped a .gitignored .env in it, asserted git status --porcelain is clean and --ignored is not, aged everything 48h, ran Clean(24h). The worktree and the .env are both gone. And Prepare auto-invokes Clean at worktrees.go:86-88, which I also confirmed removes a pre-existing legacy worktree — so this fires on the very first zero worktrees prepare or zero exec --worktree after upgrade, with no warning. Clean doesn't exist on main at all, so this is entirely introduced by this PR.

Fix is a reorder: determine ownership/legacy first, then pass includeIgnored = expiredLease || migratedLegacy. A worktree that predates the release protocol should get the same benefit of the doubt as a crashed lease.

Related: internal/worktrees/worktrees_test.go:1935 annotates its third fakeRunner result // status --porcelain --ignored: clean, but the command actually issued on that path is git status --porcelain — I added a runner.commandLine(2) assertion to the same fixture to confirm. The test asserts nothing about it, so nothing pins this either way.

Smaller things I'd want fixed in the same pass

internal/worktrees/worktrees_windows.go:39openProcessErrorMeansAlive returns alive only for ERROR_ACCESS_DENIED; everything else reads as dead. Probed:

ERROR_ACCESS_DENIED         -> alive=true
ERROR_INVALID_PARAMETER     -> alive=false
ERROR_NOT_ENOUGH_MEMORY     -> alive=false
ERROR_NO_SYSTEM_RESOURCES   -> alive=false
ERROR_TOO_MANY_OPEN_FILES   -> alive=false

processAlive's own doc at worktrees.go:206-212 promises the opposite ("any ambiguity ... counts as alive"), and the POSIX sibling honours it. Under handle or memory pressure a live run's lease reads as expired and Clean can unlock and worktree remove --force the workspace out from under it. return !errors.Is(err, windows.ERROR_INVALID_PARAMETER) restores the contract.

internal/cli/workflows.go:171runWorktreesRelease carefully redacts the release error and the success line, but forwards resolveWorkspaceRoot's error (and filepath.Abs's at :155) verbatim:

$ zero worktrees release -C /no/such/sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu <path>
[zero] cwd must be an existing directory: ...\no\such\sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu

Wrap both in redactCLIString.

internal/worktrees/worktrees.go:153 — the reuse path never reclaims a dead-owner lease even though Clean knows how at :860-865. Probed: Prepare with a dead LeasePID, then Prepare again with the same name and a live PID, and the second call fails with "locked by another active run". So a SIGKILLed zero exec --worktree --worktree-name X bricks that name until someone runs release by hand. The error message is actionable so this isn't severe, but the asymmetry is worth closing: if leasePID(reason) names a dead process, unlock and retry.

Notes, no action needed

  • Result.LockAcquired's comment says it's false when a reused worktree is already locked by another live caller, but that case returns an error at :157/:217, so it's never observably false. The if preparedWorktree.LockAcquired guard at internal/cli/exec.go:222 is dead in practice — either make the reuse path really return false, or simplify both.
  • refs/zero/orphaned-worktree/<sha> accumulates with nothing pruning it, which also pins the objects forever.
  • Prepare skips the auto-Clean when options.RunGit != nil, which is exactly what every unit test sets, so that wiring has no coverage — I needed a real-git probe to see it.
  • TestBuildServeScopeKeepsLexicalPaths fails on my box for lack of symlink privilege; pre-existing and unrelated. Everything else in internal/secrets, internal/redaction, internal/worktrees, internal/tools and internal/cli passes, and go build ./... / go vet are clean.

Unrelated to the code: I approved #857 earlier today and meant what I said there. Two of the things below are the classes that section names — a test fixture adjusted to fit a narrowed pattern, and a claim in the description that's wider than what shipped — so this is me applying your own list, not moving goalposts. Everything I found is above in one pass; there's no second round waiting behind it.

…safety

Address human review on Gitlawb#855: keep sk- bodies with a digit filter instead
of enumerated vendor prefixes, add a looser JWT form, restore the
sk-test fixture, probe legacy ownership before dirty, treat non-INVALID
Windows OpenProcess errors as alive, redact Abs/cwd release errors, and
reclaim dead-owner leases on Prepare reuse.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/redaction/redaction.go`:
- Around line 71-74: Update the redaction logic around openaiKeyPattern and
secretMatchHasDigit so known OpenAI prefixes such as sk-proj-, sk-svcacct-, and
sk-admin- are redacted even when the token contains no digits, while retaining
the digit requirement for unknown sk- forms. Add a regression case covering an
alphabet-only sk-proj- token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a571c6c5-c539-40ba-86b2-e95d54bb3bd2

📥 Commits

Reviewing files that changed from the base of the PR and between c9a347a and 7be0e6f.

📒 Files selected for processing (26)
  • internal/cli/backends_test.go
  • internal/cli/extensions_test.go
  • internal/cli/hooks_manage_test.go
  • internal/cli/mcp_commands_test.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/redaction/redaction_test.go
  • internal/secrets/boundary_test.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/selfverify/contracts_test.go
  • internal/selfverify/selfverify_test.go
  • internal/sessions/replay_test.go
  • internal/verify/contracts_test.go
  • internal/verify/verify_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_test.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees_windows_test.go
  • internal/zerocommands/backend_doctor_test.go
  • internal/zerocommands/backend_snapshots_test.go
  • internal/zerocommands/contracts_test.go
  • internal/zerogit/contracts_test.go
  • internal/zerogit/zerogit_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/cli/workflow_test.go
  • internal/secrets/scanner.go
  • internal/worktrees/worktrees_test.go
  • internal/cli/workflows.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees.go

Comment thread internal/redaction/redaction.go Outdated
Alphabet-only sk-proj-/sk-svcacct-/sk-admin- tokens are still credentials;
keep the digit filter only for unknown sk- vendor forms so kebab phrases
like sk-learn-… stay un-redacted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/secrets/scanner_test.go (1)

149-170: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Require complete secret replacement in all new redaction assertions.

These tests can pass when a matcher removes only one fragment. Require the complete credential or JWT to be absent and the expected replacement marker to be present.

  • internal/secrets/scanner_test.go#L149-L170: assert full compact JWS/JWE replacement, including payload, signature, ciphertext, and tag.
  • internal/secrets/scanner_test.go#L287-L303: assert Redact("token="+key) equals "token=[REDACTED:openai_key]".
  • internal/redaction/audit_fixes_test.go#L164-L174: assert RedactString("token="+secret, Options{}) equals "token="+RedactedSecret.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/secrets/scanner_test.go` around lines 149 - 170, Strengthen the
redaction assertions across all listed sites: in TestScanDetectsLooseJWTForms at
internal/secrets/scanner_test.go:149-170, require the complete compact JWS/JWE
token—including payload, signature, ciphertext, and tag—to be absent and the
expected replacement marker to be present; at
internal/secrets/scanner_test.go:287-303, require Redact("token="+key) to equal
"token=[REDACTED:openai_key]"; and at
internal/redaction/audit_fixes_test.go:164-174, require
RedactString("token="+secret, Options{}) to equal "token="+RedactedSecret.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/secrets/scanner_test.go`:
- Around line 149-170: Strengthen the redaction assertions across all listed
sites: in TestScanDetectsLooseJWTForms at
internal/secrets/scanner_test.go:149-170, require the complete compact JWS/JWE
token—including payload, signature, ciphertext, and tag—to be absent and the
expected replacement marker to be present; at
internal/secrets/scanner_test.go:287-303, require Redact("token="+key) to equal
"token=[REDACTED:openai_key]"; and at
internal/redaction/audit_fixes_test.go:164-174, require
RedactString("token="+secret, Options{}) to equal "token="+RedactedSecret.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c70f92e-c56d-4c0a-bbf2-3386a9e85c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 7be0e6f and ebc83b7.

📒 Files selected for processing (4)
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/secrets/scanner.go
  • internal/redaction/redaction.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Review items 1-6 are on tip (7be0e6fc + ebc83b75): broad sk- with digit filter + known prefixes always redact; looser JWT alternative; legacy Clean uses includeIgnored; Windows processAlive fail-closed; worktrees release redacts cwd errors; Prepare reclaims dead-owner leases.

Ready for re-review.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — most of this is genuinely fixed, and the worktrees half I have no complaints about. Two things are still open on the secrets side, and they're the same two.

How I checked the redaction change: I built a differential harness and ran the same sentinel corpus through secrets.Redact and redaction.RedactString on origin/main (8e26679) and on this head (ebc83b7), then diffed the two outputs key by key. That's the only way to tell a widening from a narrowing when both are happening in the same patch.

1. Broadening the pattern fixed most of it, but one shape still leaks.

The good news first — everything the previous head dropped is back: sk-proj-, sk-svcacct-, sk-admin-, sk-or-v1-, sk-fw-, and plain digit-bearing sk-. And the false positives genuinely improved: sk-learn-machine-learning-model-pipeline, sk-this-is-a-normal-kebab-case-identifier and /home/user/projects/sk-utils-library-v2/main.go were all redacted on main and are correctly left alone here. That's a real win.

What the digit filter costs: a digit-free sk- body now passes straight through, both in Scan and in RedactString.

in:  prefix sk-AAAAAA…(48 A's) suffix
main: prefix [REDACTED:openai_key] suffix
head: prefix sk-AAAAAA…(48 A's) suffix

Legacy OpenAI keys are sk- + 48 base62 characters, so about one in 4,500 of them contains no digit at all — (52/62)^48. That's rare, but it's a silent full-credential leak, not a truncation, and it's the exact failure mode this PR set out to close.

The filter is aimed at kebab phrases, and kebab phrases have hyphens. Real legacy keys don't. So gate on that instead:

if p.typ == "openai_key" && !knownOpenAIKeyPrefix(m) && !containsDigit(m) &&
    strings.Contains(strings.TrimPrefix(m, "sk-"), "-") {
    continue
}

I applied that and re-ran the corpus: the two digit-free legacy shapes come back, and nothing else moves — every kebab false positive above stays un-redacted. Mirror it in redaction.go's secretMatchHasDigit twin.

2. The fixture is still weakened, and it's what's hiding item 1.

internal/secrets/boundary_test.go still differs from main. I checked out main's copy of the file onto this head and ran it:

boundary_test.go:35: real secret NOT caught in "token: \"sk-abcdefghijklmnopqrstuvwxyz\""

That's the same shape as the leak above. Appending 0 to the fixture is what made the test agree with the new pattern rather than the other way round — which is what I asked us not to do last time. With the hyphen gate applied, main's original fixture passes unmodified, so nothing needs changing there at all.

While I was in it, I probed the other three fixture edits individually. All three pass with main's original values:

findings=1 in="export OPENAI_API_KEY=sk-proj-abcdefghijklmnopqrstuvwx"
findings=1 in="key is sk-svcacct-abcdefghijklmnopqrstuvwx at the end"
findings=1 in="    secret_test.go:12: token sk-proj-abcdefghijklmnopqrstuvwxyz"

Those are known prefixes, so the digit filter never applied to them. The 0s in boundary_test.go, redaction_test.go and audit_fixes_test.go are churn — please revert them so the diff shows only the one fixture that genuinely had to move (and after the fix, none of them do).

One thing that isn't in my original list but should be in the PR body. Aligning RedactString to secrets.Scan also raised its floors relative to main: github_pat 12→22, gh[pousr]_ 12→36 with _ dropped from the body class, AIza 12→35, sk- 12→20 with . dropped, JWT segments 8→10. Each of those is a shape main redacted and this head doesn't — e.g. ghp_abcd_efghijklmnopqrstuvwxyz0123456789 now passes through whole. I'm fine with all of it, since the new floors match real issued key lengths and a single source of truth is the right call. But it arrived unannounced inside a fix for the opposite problem, and that's how a narrowing slips past review. Please say so in the summary.

3, 4, 5, 6, 7, 8 — all closed. I mutation-tested each one I'm accepting:

  • Ordering ownership/legacy detection ahead of the dirty probe is right, and includeIgnored := expiredLease || legacy is load-bearing: dropping || legacy turns both legacy tests red with the correct message.
  • firstStatusCommand() is a real assertion now, and it fails under the same mutation.
  • JWT: the loose alternative does what it should. A non-JSON-payload JWS is detected where main detected nothing, and a five-segment JWE now has its header, encrypted CEK and IV redacted. Ciphertext and tag survive, which I'm happy with — without the CEK they're inert.
  • openProcessErrorMeansAlive is correctly fail-closed. Reverting it to errors.Is(err, ERROR_ACCESS_DENIED) fails TestOpenProcessErrorMeansAliveOnAmbiguousErrors. Good call including TestOsProcessAliveReportsDeadAfterExit against a real exited process rather than only errno constants — that distinction has bitten us on Windows before.
  • Release error redaction: I ran -C <bad>, --cwd=<bad>, the releaseWorktree failure and the success echo through the real CLI entry point. All four redact.
  • Dead-owner reclaim: stubbing the reclaim branch to if false turns TestPrepareReclaimsDeadOwnerLeaseOnReuse red.

One gap on 8. Nothing tests the safety direction — that a lease held by a live pid is still rejected and never unlocked. TestPrepareRejectsWorktreeLockedByAnotherRun doesn't get there: its fakeRunner runs out of queued results, so reclaimDeadOwnerLease sees an empty worktree list and returns early. It never reaches the processAlive(pid) branch. I wrote that test locally and it passes, so this is about pinning behaviour, not a bug — but over-reclaiming would hand one checkout to two live runs, which is worse than the bug we started with. Please add it.

Two nits, take or leave: the strict jwt pattern is a strict subset of the loose one, so the first entry is dead code; and unknown worktrees release flag %q echoes the raw argument unredacted, though that matches what every other command in main already does.

Fix 1 and 2 and I'll approve. Everything else here is solid work.

Local run: go test ./internal/worktrees/... ./internal/secrets/... ./internal/redaction/... ./internal/cli/... ./internal/tools/... green apart from TestBuildServeScopeKeepsLexicalPaths, which needs symlink privileges and fails on my box regardless of branch. gofmt clean.

Everything above is one pass — six of the eight are genuinely closed and I've said which. The two that remain are really one thing: the digit filter and the fixture that hides it. The hyphen gate closes both, and then main's original fixture passes untouched, so this should be a small change rather than another round of pattern tuning.

@Vasanthdev2004

Vasanthdev2004 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@euxaristia one thing from the #855 review I want to pull out on its own, because it's easy to lose in a list of eight and it's the bit that actually matters.

The fixture in internal/secrets/boundary_test.go moved again. Last round a 0 was appended to sk-abcdefghijklmnopqrstuvwxyz so it matched the narrowed pattern; this round it's still carrying that 0. I'm not raising it as a process complaint I'm raising it because it's the mechanism by which the leak survived. Check out main's copy of that file onto your head and it fails with "real secret NOT caught", on exactly the shape that still leaks. The test was the only thing standing between that pattern and review, and adjusting it to agree made the pattern look correct.

That's a genuinely easy trap. When a test fails after a pattern change, the fixture is usually the fastest thing to move, and it's often right to move it. Here it happened to be the alarm rather than the noise.

Worth saying: the other three fixture edits I asked about turned out to be unnecessary sk-proj-, sk-svcacct- and the audit one all pass with main's original values, because the digit filter never applied to known prefixes. So the diff only ever needed the one fixture to move, and with the hyphen gate it doesn't need that either.

None of this changes how the week has gone from my side. You've turned around seven PRs in a day and taken the harder fix over the easy one more than once the beforeTool veto on #853 being the clearest case, where I'd offered you the cheap option and you closed the gap properly instead. This is one line in one file, not a pattern I'm worried about.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/command_output_test.go (1)

101-114: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep the original regression inputs and verify complete secret removal.

All changed assertions reject only the complete new value. A prefix-only replacement can leave a credential tail while these tests pass.

  • internal/tui/command_output_test.go#L101-L114: retain the shorter Google-token case and assert that no token suffix remains.
  • internal/tui/command_output_test.go#L190-L198: apply the same complete-removal check to formatted command output.
  • internal/tui/command_polish_test.go#L256-L256: retain the previous shorter sandbox-grant fixture.
  • internal/tui/command_polish_test.go#L315-L315: reject both the complete credential and any leaked suffix.
  • internal/tui/model_test.go#L375-L375: retain the previous shorter sandbox-grant fixture.
  • internal/tui/model_test.go#L409-L409: reject both the complete credential and any leaked suffix.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/command_output_test.go` around lines 101 - 114, Preserve the
original shorter regression fixtures and strengthen complete-removal assertions:
in internal/tui/command_output_test.go lines 101-114, retain the shorter
Google-token case and reject its leaked suffix; in lines 190-198, apply the same
suffix check to formatted command output; retain the previous shorter
sandbox-grant fixtures in internal/tui/command_polish_test.go line 256 and
internal/tui/model_test.go line 375; update internal/tui/command_polish_test.go
line 315 and internal/tui/model_test.go line 409 to reject both each complete
credential and any leaked suffix.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/tui/command_output_test.go`:
- Around line 101-114: Preserve the original shorter regression fixtures and
strengthen complete-removal assertions: in internal/tui/command_output_test.go
lines 101-114, retain the shorter Google-token case and reject its leaked
suffix; in lines 190-198, apply the same suffix check to formatted command
output; retain the previous shorter sandbox-grant fixtures in
internal/tui/command_polish_test.go line 256 and internal/tui/model_test.go line
375; update internal/tui/command_polish_test.go line 315 and
internal/tui/model_test.go line 409 to reject both each complete credential and
any leaked suffix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6790eed6-418f-4af2-8f35-fc89b6f03afc

📥 Commits

Reviewing files that changed from the base of the PR and between ebc83b7 and d2030d6.

📒 Files selected for processing (7)
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/tui/command_output_test.go
  • internal/tui/command_polish_test.go
  • internal/tui/model_test.go
  • internal/worktrees/worktrees_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/secrets/scanner.go
  • internal/worktrees/worktrees_test.go
  • internal/redaction/redaction.go
  • internal/redaction/audit_fixes_test.go

Vasanthdev2004
Vasanthdev2004 previously approved these changes Aug 3, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Both remaining items are closed, and I checked the redaction change in both directions rather than reading the diff — a wider pattern can be wrong too, and the only thing that tells a fix from an over-correction is running real shapes through it.

The hyphen gate does what it needs to. Running the corpus through secrets.Redact on this head:

digit-free legacy -> "prefix [REDACTED:openai_key] suffix"
kebab identifier  -> "sk-this-is-a-normal-kebab-case-identifier"

So the leak is closed and the false positives stay fixed. Everything that must redact does — digit-free legacy, digit-bearing legacy, sk-proj-, sk-svcacct-, and the fixture itself — and everything that must not still doesn't: the kebab identifier, sk-learn-machine-learning-model-pipeline, and the sk-utils-library-v2 path. Removing the gate makes the digit-free key leak again, so it's genuinely load-bearing rather than incidental. Both sides carry it, scanner.go:77 and the redaction.go twin, which was the other half I'd have checked.

The fixtures are properly back. git diff origin/main..HEAD -- internal/secrets/boundary_test.go is empty, so boundary_test.go is byte-identical to main — no residue from either round of adjustment, and the churn in the other two files is gone too. That's the outcome I was after: main's original fixture passing untouched against the corrected pattern, rather than a fixture shaped to fit whatever the pattern currently does.

internal/secrets, internal/redaction and internal/worktrees are all green here.

Thanks for taking the fixture point seriously rather than treating it as pedantry — that test is the only thing standing between this pattern and the next reviewer, and it's worth more now than it was two rounds ago.

@euxaristia

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
internal/worktrees/worktrees_test.go (1)

1942-1981: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a failure case for the unlock in this path.

This test asserts that a dead-lease entry with a missing directory is unlocked and pruned. It only covers the success path. The source discards the unlock error here (see my comment on worktrees.go lines 880-888), so no test can currently detect that regression.

When you fix the source, add a companion case where the unlock returns a nonzero exit and assert that Clean returns an error naming the entry.

As per coding guidelines: "Every behavior or security-boundary change requires a regression test, including failure paths."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_test.go` around lines 1942 - 1981, Add a
companion test alongside TestCleanUnlocksExpiredLeaseBeforePruningMissingDir
where the fakeRunner returns a nonzero exit for the unlock command, then assert
Clean returns an error that identifies the affected missing-dead-task entry.
Keep the existing success-path test unchanged and ensure the failure case
verifies the error is propagated rather than proceeding silently.

Source: Coding guidelines

internal/cli/workflow_test.go (3)

231-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two failure branches of runWorktreesRelease are untested.

The parser rejects a duplicate positional path with "worktree path was provided more than once" (workflows.go line 139) and an unrecognized flag with "unknown worktrees release flag" (line 136). Neither has a test. Both are cheap to add alongside this missing-path case.

As per coding guidelines: "Every behavior or security-boundary change requires a regression test, including failure paths."

💚 Cover the remaining parser rejections
func TestRunWorktreesReleaseRejectsDuplicatePath(t *testing.T) {
	var stdout, stderr bytes.Buffer
	exitCode := runWithDeps([]string{"worktrees", "release", "/a", "/b"}, &stdout, &stderr, appDeps{
		releaseWorktree: func(context.Context, worktrees.Options, string) error {
			t.Fatal("releaseWorktree must not run for an ambiguous path argument")
			return nil
		},
	})

	if exitCode != exitUsage {
		t.Fatalf("expected usage exit %d, got %d", exitUsage, exitCode)
	}
	if !strings.Contains(stderr.String(), "more than once") {
		t.Fatalf("expected duplicate-path error, got %q", stderr.String())
	}
}

func TestRunWorktreesReleaseRejectsUnknownFlag(t *testing.T) {
	var stdout, stderr bytes.Buffer
	exitCode := runWithDeps([]string{"worktrees", "release", "--nope", "/a"}, &stdout, &stderr, appDeps{
		releaseWorktree: func(context.Context, worktrees.Options, string) error {
			t.Fatal("releaseWorktree must not run for an unknown flag")
			return nil
		},
	})

	if exitCode != exitUsage {
		t.Fatalf("expected usage exit %d, got %d", exitUsage, exitCode)
	}
	if !strings.Contains(stderr.String(), "unknown worktrees release flag") {
		t.Fatalf("expected unknown-flag error, got %q", stderr.String())
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 231 - 247, Add regression tests
alongside TestRunWorktreesReleaseRequiresPath for runWorktreesRelease parser
failures: verify duplicate positional paths return exitUsage, emit the “more
than once” error, and do not call releaseWorktree; also verify an unknown flag
returns exitUsage, emits the unknown-flag error, and does not call
releaseWorktree.

Source: Coding guidelines


813-853: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the Options.Cwd that exec passes to releaseWorktree.

exec.go line 224 deliberately passes trustRoot, the original launch directory captured before workspaceRoot is reassigned to the worktree path. That choice matters: Release uses Options.Cwd as git's working directory when the worktree directory is gone. If it were changed to workspaceRoot, git would run from inside the deleted worktree and the recovery path would break silently.

No test pins this. Add the assertion here, where the release callback already captures its arguments.

💚 Pin the trust-root wiring
 	root := t.TempDir()
 	worktreeDir := t.TempDir()
 	var releasedPath string
+	var releasedCwd string
 	releaseCalls := 0
@@
 		releaseWorktree: func(ctx context.Context, options worktrees.Options, path string) error {
 			releaseCalls++
 			releasedPath = path
+			releasedCwd = options.Cwd
 			return nil
 		},
@@
 	if releasedPath != worktreeDir {
 		t.Fatalf("released path = %q, want %q", releasedPath, worktreeDir)
 	}
+	// Release runs git from Options.Cwd when the worktree directory is gone,
+	// so exec must pass the original launch directory, not the worktree it
+	// reassigned workspaceRoot to.
+	if releasedCwd != root {
+		t.Fatalf("release Options.Cwd = %q, want the trust root %q", releasedCwd, root)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 813 - 853, Extend
TestRunExecWorktreeReleasesLockAfterRun to capture the releaseWorktree options
argument and assert that Options.Cwd equals root, the original launch directory.
Keep the existing call-count and released-path assertions, and verify the
trust-root value passed by exec rather than the reassigned worktree workspace
path.

181-229: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use t.Chdir for the working-directory change.

go.mod declares Go 1.26.5, so t.Chdir(parent) is available and removes the manual os.Getwd/os.Chdir cleanup from this test.

♻️ Use `t.Chdir`
-	origWd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
 	parent := t.TempDir()
 	worktreeDir := filepath.Join(parent, "task-a")
 	if err := os.Mkdir(worktreeDir, 0o700); err != nil {
 		t.Fatal(err)
 	}
-	if err := os.Chdir(parent); err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		if err := os.Chdir(origWd); err != nil {
-			t.Fatal(err)
-		}
-	}()
+	t.Chdir(parent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 181 - 229, Update
TestRunWorktreesReleaseNormalizesRelativePath to use t.Chdir(parent) instead of
manually capturing the current directory, calling os.Chdir, and restoring it
with defer. Remove the now-unnecessary origWd handling and cleanup while
preserving the test’s expected absolute-path behavior.
internal/worktrees/worktrees.go (1)

82-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The cleanup gate keys off an injected runner, which couples production behavior to test wiring.

options.RunGit == nil is true only for the default runner. Any embedder that injects its own GitRunner (a sandboxed or instrumented git wrapper) silently loses automatic stale cleanup, even though nothing about a custom runner makes cleanup unsafe. The real intent is "tests do not want the extra git calls".

Consider an explicit option (for example SkipAutoClean bool) so the behavior is named rather than inferred from which runner was supplied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees.go` around lines 82 - 88, Replace the
options.RunGit == nil cleanup gate in the worktree request flow with an explicit
option such as SkipAutoClean. Run Clean with the existing 24-hour threshold by
default, including when a custom GitRunner is injected, and skip only when the
explicit test-oriented option is enabled.
internal/worktrees/worktrees_posix.go (1)

17-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The logic is correct, but this branch has no direct unit test while its Windows sibling does.

worktrees_windows_test.go covers the classification of each error case. There is no worktrees_posix_test.go. The ESRCH and EPERM branches are only exercised indirectly through Clean and Prepare.

Add a small POSIX test file that asserts the live-self case and the exited-child case, mirroring TestOsProcessAliveReportsLiveSelf and TestOsProcessAliveReportsDeadAfterExit.

As per coding guidelines: "Every behavior or security-boundary change requires a regression test, including failure paths; path-sensitive logic must include a non-Linux case or a hermetic equivalent exercising the same normalization."

💚 Suggested `internal/worktrees/worktrees_posix_test.go`
//go:build !windows

package worktrees

import (
	"os"
	"os/exec"
	"testing"
)

func TestOsProcessAliveReportsLiveSelf(t *testing.T) {
	if !osProcessAlive(os.Getpid()) {
		t.Fatal("current process must report alive")
	}
}

func TestOsProcessAliveReportsDeadAfterExit(t *testing.T) {
	cmd := exec.Command("/bin/sh", "-c", "exit 0")
	if err := cmd.Start(); err != nil {
		t.Skipf("start child process: %v", err)
	}
	pid := cmd.Process.Pid
	if err := cmd.Wait(); err != nil {
		t.Fatalf("wait for child process: %v", err)
	}
	// Wait reaps the child, so the PID no longer names a process.
	if osProcessAlive(pid) {
		t.Fatal("exited and reaped process must not report alive")
	}
}

// PID 1 exists but is not signalable by an unprivileged caller, so the EPERM
// branch must classify it as alive rather than expiring a lease.
func TestOsProcessAliveTreatsPermissionDeniedAsAlive(t *testing.T) {
	if os.Geteuid() == 0 {
		t.Skip("running as root: signal 0 to pid 1 succeeds instead of returning EPERM")
	}
	if !osProcessAlive(1) {
		t.Fatal("a process we may not signal must be treated as alive")
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_posix.go` around lines 17 - 32, Add a POSIX-only
test file covering osProcessAlive with the current process and a
started-then-reaped child, asserting alive and dead results respectively. Also
cover the permission-denied case for PID 1 when running unprivileged, skipping
under root, to exercise the EPERM classification. Mirror the existing Windows
test naming and use a portable child-process command.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/workflows.go`:
- Around line 889-892: Update the release command help text for the -C/--cwd
option in the release flags block to describe it as needed only when the source
repository cannot be resolved from the current directory, rather than claiming
it is required whenever the worktree was deleted. Keep the description
consistent with the best-effort launch-directory resolution behavior.

In `@internal/secrets/scanner.go`:
- Around line 57-61: Update internal/secrets/scanner.go lines 57-61 to replace
the overlapping JWT patterns with one expression matching either a three-segment
JWS or all five segments of a compact JWE. Update
internal/secrets/scanner_test.go lines 149-169 so every token assertion requires
redacted to equal "auth=[REDACTED:jwt]", covering the complete JWE tail and
preventing leaked ciphertext or authentication tags.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1278-1312: Fix both Clean test fixtures in
internal/worktrees/worktrees_test.go#L1278-L1312 and `#L1321-L1356`: include
worktree <repoRoot> first in each porcelain listing, wrap tempDir with
physicalTestPath, and in the first fixture move lockedPath under the derived
repoDir. In both TestCleanSkipsLockedWorktree and the sibling PID-less lease
test, assert call 2 is git worktree prune so the intended guard logic is
actually exercised.

In `@internal/worktrees/worktrees.go`:
- Around line 880-888: Update the missing-directory branch in Clean around the
expiredLease unlock to capture the worktree unlock error in lastErr instead of
discarding it. When unlock fails, skip worktree prune for that entry; preserve
pruning only after a successful unlock, matching the existing handling near the
later cleanup path.
- Around line 401-410: Update writeOwnershipMarker to avoid writing directly to
the marker path: create and fully write a temporary file in the same gitDir,
then atomically rename it over zeroOwnerMarkerFile. Ensure temporary-file
cleanup on failure while preserving the existing error-wrapping behavior.
- Around line 1022-1032: Update preserveUnreachableWorktreeHead to distinguish
an unborn worktree from other git rev-parse failures by using a
verification/parsing approach that identifies the expected unborn-HEAD case.
Return nil only when HEAD is genuinely absent; propagate a descriptive error for
any other probe failure so Clean does not proceed to force removal.
- Around line 292-308: Update lockWorktree’s git invocation to force LC_ALL=C
when running “worktree lock”, ensuring output remains English before checking
for “already locked”. Preserve the existing exit-code handling and error parsing
behavior.

In `@internal/zerocommands/backend_snapshots_test.go`:
- Line 92: Restore alphabet-only sk-proj- fixtures at
internal/zerocommands/backend_snapshots_test.go:92 and :204/:403,
internal/verify/contracts_test.go:11, internal/verify/verify_test.go:94-111,
internal/zerocommands/backend_doctor_test.go:14,
internal/zerocommands/contracts_test.go:55, and
internal/zerogit/contracts_test.go:9; in verify_test.go, restore both the
structured-output fixture and its assertion. Add regression coverage for each
affected security-boundary behavior, preserving the digit-free known OpenAI
prefix cases.

---

Nitpick comments:
In `@internal/cli/workflow_test.go`:
- Around line 231-247: Add regression tests alongside
TestRunWorktreesReleaseRequiresPath for runWorktreesRelease parser failures:
verify duplicate positional paths return exitUsage, emit the “more than once”
error, and do not call releaseWorktree; also verify an unknown flag returns
exitUsage, emits the unknown-flag error, and does not call releaseWorktree.
- Around line 813-853: Extend TestRunExecWorktreeReleasesLockAfterRun to capture
the releaseWorktree options argument and assert that Options.Cwd equals root,
the original launch directory. Keep the existing call-count and released-path
assertions, and verify the trust-root value passed by exec rather than the
reassigned worktree workspace path.
- Around line 181-229: Update TestRunWorktreesReleaseNormalizesRelativePath to
use t.Chdir(parent) instead of manually capturing the current directory, calling
os.Chdir, and restoring it with defer. Remove the now-unnecessary origWd
handling and cleanup while preserving the test’s expected absolute-path
behavior.

In `@internal/worktrees/worktrees_posix.go`:
- Around line 17-32: Add a POSIX-only test file covering osProcessAlive with the
current process and a started-then-reaped child, asserting alive and dead
results respectively. Also cover the permission-denied case for PID 1 when
running unprivileged, skipping under root, to exercise the EPERM classification.
Mirror the existing Windows test naming and use a portable child-process
command.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1942-1981: Add a companion test alongside
TestCleanUnlocksExpiredLeaseBeforePruningMissingDir where the fakeRunner returns
a nonzero exit for the unlock command, then assert Clean returns an error that
identifies the affected missing-dead-task entry. Keep the existing success-path
test unchanged and ensure the failure case verifies the error is propagated
rather than proceeding silently.

In `@internal/worktrees/worktrees.go`:
- Around line 82-88: Replace the options.RunGit == nil cleanup gate in the
worktree request flow with an explicit option such as SkipAutoClean. Run Clean
with the existing 24-hour threshold by default, including when a custom
GitRunner is injected, and skip only when the explicit test-oriented option is
enabled.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6dbae519-2fa0-4658-ab61-f96e2c347387

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and d2030d6.

📒 Files selected for processing (33)
  • internal/cli/app.go
  • internal/cli/backends_test.go
  • internal/cli/completions.go
  • internal/cli/completions_test.go
  • internal/cli/exec.go
  • internal/cli/extensions_test.go
  • internal/cli/hooks_manage_test.go
  • internal/cli/mcp_commands_test.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/selfverify/contracts_test.go
  • internal/selfverify/selfverify_test.go
  • internal/sessions/replay_test.go
  • internal/tools/bash_secrets_test.go
  • internal/tui/command_output_test.go
  • internal/tui/command_polish_test.go
  • internal/tui/model_test.go
  • internal/verify/contracts_test.go
  • internal/verify/verify_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_posix.go
  • internal/worktrees/worktrees_test.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees_windows_test.go
  • internal/zerocommands/backend_doctor_test.go
  • internal/zerocommands/backend_snapshots_test.go
  • internal/zerocommands/contracts_test.go
  • internal/zerogit/contracts_test.go
  • internal/zerogit/zerogit_test.go

Comment thread internal/cli/workflows.go
Comment thread internal/secrets/scanner.go Outdated
Comment thread internal/worktrees/worktrees_test.go
Comment thread internal/worktrees/worktrees.go
Comment thread internal/worktrees/worktrees.go
Comment thread internal/worktrees/worktrees.go
Comment thread internal/worktrees/worktrees.go
Comment thread internal/zerocommands/backend_snapshots_test.go Outdated
Redact full compact JWE tokens, pin git locale for lock parsing, write the
ownership marker atomically, surface missing-dir unlock failures, and
fail closed when HEAD probes are indeterminate. Align Clean fixtures and
digit-free known-prefix redaction tests with the shipped behavior.

Refs Gitlawb#855
@euxaristia

Copy link
Copy Markdown
Contributor Author

CodeRabbit follow-up (a29900fb)

Addressed the open review findings on this tip:

Severity Finding Status
Major Redact all five compact JWE segments Fixed + tests
Major Pin git locale (LC_ALL=C) for lock parse Fixed in defaultRunGit
Major Atomic ownership marker write Fixed (temp + rename)
Major Clean missing-dir unlock error discarded Fixed + test
Major HEAD probe fail-closed before force-remove Fixed + tests
Minor release -C help overstates requirement Fixed
Minor Clean fixtures omit main worktree Fixed
Minor Digit-free sk-proj- fixtures Restored

Validation: go test on secrets, worktrees, zerocommands, verify, zerogit, redaction, and focused cli suites. Full cli package has a pre-existing WSL bash completions failure in this environment (unrelated).

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Rate Limit Exceeded

@euxaristia have exceeded the limit for the number of chat messages per hour. Please wait 19 minutes and 6 seconds before sending another message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants